/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package fhv.itb.mst2955.client.controllers;

import com.google.gwt.core.client.GWT;
import com.google.gwt.gears.client.database.Database;
import com.google.gwt.gears.client.database.DatabaseException;
import com.google.gwt.gears.client.database.ResultSet;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import fhv.itb.mst2955.client.AddressService;
import fhv.itb.mst2955.client.AddressServiceAsync;
import fhv.itb.mst2955.client.ContactService;
import fhv.itb.mst2955.client.ContactServiceAsync;
import fhv.itb.mst2955.client.SyncService;
import fhv.itb.mst2955.client.SyncServiceAsync;
import fhv.itb.mst2955.client.ui.AddressBookMain;
import fhv.itb.mst2955.client.event.UpdateEventListener;
import fhv.itb.mst2955.client.model.AddressProxy;
import fhv.itb.mst2955.client.model.ChangeObject;
import fhv.itb.mst2955.client.model.ContactProxy;
import fhv.itb.mst2955.client.model.CountryProxy;
import fhv.itb.mst2955.client.model.UpdateObject;
import fhv.itb.mst2955.client.ui.HandleConflictsDialog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;


/**
 * The controller of the application.
 * This class contains in-memory lists of domain objects,
 * handles client-side persistence activity and control
 * communication with the server.
 */
public class ContactController {
    private static ContactController instance;
    private static List<UpdateEventListener> updateListeners = new ArrayList<UpdateEventListener>();

    private Timer syncTimer;

    private Database db;
    HashMap<Integer, ContactProxy> contacts;
    HashMap<Integer, AddressProxy> addresses;
    HashMap<Integer, CountryProxy> countries;

    private Vector<ChangeObject> syncConflicts;

    private Integer nextContactID;
    private Integer nextAddressID;


    private ContactController() {
        addresses = new HashMap<Integer, AddressProxy>();
        contacts = new HashMap<Integer, ContactProxy>();
        countries = new HashMap<Integer, CountryProxy>();

        syncConflicts = new Vector<ChangeObject>();

        // make sure the database tables exists
        db = AddressBookMain.getDb();
        buildLocalDatabase();
    }


    /**
     *
     * @return The singleton instance of this class.
     */
    public static ContactController getInstance() {
        if (instance == null) instance = new ContactController();
        return instance;
    }


    /**
     * Initializes the in-memory lists of contacts and addresses,
     * and starts a synchronization thread.
     */
    public void init() {
//        rebuildLocalDatabase();

        // fill lists from local database
        getFromLocalAddresses();
        getFromLocalContacts();

        fireUpdateEvent();

        // synchronize local data with remote database
        synchronize();
 

    // regularly synchronize in-memory list to local db and remote db
        syncTimer = new Timer() {
            @Override
            public void run() {
                synchronize();
            }
        };
        syncTimer.scheduleRepeating(5000);
    }


    /**
     * For development only.
     */
    public void rebuildLocalDatabase() {
        try {
            db.execute("drop table if exists contact");
            db.execute("drop table if exists address");
            db.execute("drop table if exists country");
            db.execute("drop table if exists changes");
            buildLocalDatabase();
            Window.alert("successfully rebuilt local database");
            // synchronize to local and remote db
//            getAddressService().putAddresses(new Vector<AddressProxy>(addresses.values()), syncAddressesCallback);
        } catch (DatabaseException ex) {
            Window.alert("Failed to drop table in local database!\n" + ex.getMessage());
        }
    }


    /**
     * Creates the database tables, if they do not already exist in the local database.
     */
    private void buildLocalDatabase() {

        try {
            db.execute("create table if not exists changes(id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    "objectID INTEGER NOT NULL, typeName VARCHAR(30) NOT NULL, changeType VARCHAR(3) NOT NULL)");
        } catch (DatabaseException ex) {
        }

        try {
            db.execute("create table if not exists country(id INTEGER PRIMARY KEY, " +
                    "name VARCHAR(60) NOT NULL, nickName VARCHAR(3) NOT NULL)");
        } catch (DatabaseException ex) {
        }

        try {
            db.execute("create table if not exists address(id INTEGER PRIMARY KEY, " +
                    "city VARCHAR(30) NOT NULL, street VARCHAR(60), zip VARCHAR(10) NOT NULL, " +
                    "countryID INTEGER NOT NULL DEFAULT 1, versionNr INTEGER NOT NULL DEFAULT 1)");
        } catch (DatabaseException ex) {
        }

        try {
            db.execute("create table if not exists contact(id INTEGER PRIMARY KEY, " +
                    "firstName VARCHAR(30) NOT NULL, lastName VARCHAR(30) NOT NULL, " +
                    "addressID INTEGER NOT NULL, versionNr INTEGER NOT NULL DEFAULT 1)");
        } catch (DatabaseException ex) {
        }
    }


    private UpdateObject getNewerVersion(UpdateObject fromDB, UpdateObject fromList) {
        return fromDB.getVersionNr() > fromList.getVersionNr() ? fromDB : fromList;
    }


    /**
     * Synchronizes the local data with the data on the remote server.
     */
    private void synchronize() {
        // synchronization can only take place if old conflicts are resolved
        if (syncConflicts.size() == 0) {
            try {
                // create the list of ChangeObjects
                Vector<ChangeObject> changedObjects = new Vector<ChangeObject>();
                ResultSet rs = db.execute("SELECT * FROM changes");

                for (int i = 0; rs.isValidRow(); ++i, rs.next()) {
                    ChangeObject co = new ChangeObject();
                    co.setObjectId(rs.getFieldAsInt(1));
                    co.setObjectType(rs.getFieldAsString(2));
                    co.setChangeType(rs.getFieldAsString(3));
                    UpdateObject uo = co.getObjectType().equals(ChangeObject.TYPE_ADDRESS) ? addresses.get(co.getObjectId()) : contacts.get(co.getObjectId());
                    if (uo != null) {
                        co.setUpdateObject(uo);
                    }
                    changedObjects.add(co);

//                Window.alert("object: " + co.getObjectType() + "\nid: " + co.getObjectId() + "\ntype: " + co.getChangeType());
                }

                // send to server
                getSyncService().update(changedObjects, remoteUpdateCallback);

            } catch (DatabaseException ex) {
                Window.alert(ex.getMessage());
            } catch (Exception e) {
                Window.alert("sync error:\n" + e.getMessage());
            }

            // get data from remote database (getAddresses and getContacts will be called
            // from within this method for synchronous data retrieval)
            // ONLY GET SERVER DATA IF NO CONFLICTS EXIST!
            if (syncConflicts.size() == 0) {
                getAddressService().getCountries(getRemoteCountries);
            }
        }
    }


    /**
     * Opens a dialog for the user to resolve synchronization conflicts.
     */
    public void handleConflicts() {
        HandleConflictsDialog dialog = new HandleConflictsDialog();
    }


    /**
     * Creates the in-memory address list from the local database.
     */
    private void getFromLocalAddresses() {
        ResultSet rs = null;

        // try to get addresses locally to build up the contact tab
        if (db != null) {

            try {
                rs = db.execute("select * from country");
                for (int i = 0; rs.isValidRow(); ++i, rs.next()) {
                    CountryProxy c = new CountryProxy();
                    c.setId(new Integer(rs.getFieldAsInt(0)));
                    c.setName(rs.getFieldAsString(1));
                    c.setNickName(rs.getFieldAsString(2));
                    countries.put(c.getId(), c);
                }
                rs.close();
            } catch (DatabaseException ex) {
                Window.alert("Failed to get countries from local database!\n" + ex.getMessage());
            } catch (Exception e) {
                Window.alert("Error while getting countries from local database!\n" + e.getMessage());
            }

            try {
                rs = db.execute("select * from address");
                for (int i = 0; rs.isValidRow(); ++i, rs.next()) {
                    AddressProxy a = new AddressProxy();
                    a.setId(new Integer(rs.getFieldAsInt(0)));
                    a.setCity(rs.getFieldAsString(1));
                    a.setStreet(rs.getFieldAsString(2));
                    a.setZip(rs.getFieldAsString(3));
                    a.setCountryID(rs.getFieldAsInt(4));
                    a.setCountry(countries.get(a.getCountryID()));
                    a.setVersionNr(rs.getFieldAsInt(5));

                    if (addresses.containsKey(a.getId())) {
                        a = (AddressProxy) getNewerVersion(a, addresses.get(a.getId()));
                    }
                    addresses.put(a.getId(), a);
                }
                rs.close();
            } catch (DatabaseException ex) {
                Window.alert("Failed to get addresses from local database!\n" + ex.getMessage());
            } catch (Exception e) {
                Window.alert("Error while getting addresses from local database\n" + e.getMessage());
            }

        }

    }


    /**
     * Creates the in-memory contact list from the local database.
     */
    private void getFromLocalContacts() {
        ResultSet rs = null;

        // try to get contacts locally to build up the contact tab
        if (db != null) {
            try {
                rs = db.execute("select * from contact");

                for (int i = 0; rs.isValidRow(); ++i, rs.next()) {
                    ContactProxy c = new ContactProxy();
                    c.setId(new Integer(rs.getFieldAsInt(0)));
                    c.setFirstName(rs.getFieldAsString(1));
                    c.setLastName(rs.getFieldAsString(2));
                    c.setAddressID(rs.getFieldAsInt(3));
                    c.setVersionNr(rs.getFieldAsInt(4));
                    contacts.put(c.getId(), c);
                }

                rs.close();
            } catch (DatabaseException ex) {
                Window.alert("Failed to get contacts from local database!\n" + ex.getMessage());
            } catch (Exception e) {
                Window.alert(e.getMessage());
            }

        }

    }


    /**
     * Writes the in-memory list of Addresses to the local database.
     */
    private void putToLocalAddresses() {
        ResultSet rs = null;
        int i = 0;

        for (CountryProxy c : countries.values()) {
            try {
                rs = db.execute("select * from country where id = ?", new String[]{c.getId().toString()});

                for (i = 0; rs.isValidRow(); ++i, rs.next()) ;

                if (i == 0) // not in db, insert
                    db.execute("insert into country (id, name, nickName) " + "values (?, ?, ?)",
                            new String[]{c.getId().toString(), c.getName(), c.getNickName()});
                rs.close();
            } catch (DatabaseException ex) {
                Window.alert(ex.getMessage());
            }
        }

        for (AddressProxy am : addresses.values()) {
            try {
                // try to find in local db
                rs = db.execute("select * from address where id = ?", new String[]{am.getId().toString()});

                for (i = 0; rs.isValidRow(); ++i, rs.next()) {

                    if (i > 1) {
                        Window.alert("More than one contact found in local db");
                        throw new Exception("More than one address found in local db");
                    }

                    if (am.getVersionNr() > rs.getFieldAsInt(4)) {
                        try {
                            db.execute("update address set city = ?, street = ?, " +
                                    "zip = ?, countryID = ?, versionNr = ? where id = ?",
                                    new String[]{am.getCity(), am.getStreet() == null ? "NULL" : am.getStreet(), am.getZip(),
                                    am.getCountryID().toString(), am.getVersionNr().toString(), am.getId().toString()});
                        } catch (DatabaseException ex) {
                            Window.alert("Cannot execute update address\n" + ex.getMessage());
                        }
                    }

                }

                if (i == 0) {
                    // insert into database
                    db.execute("insert into address (id, city, street, zip, countryID) " + "values (?, ?, ?, ?, ?)",
                            new String[]{am.getId().toString(), am.getCity(), am.getStreet() == null ? "NULL" : am.getStreet(),
                            am.getZip(), am.getCountryID().toString()});
                }

                rs.close();
            } catch (DatabaseException ex) {
                Window.alert(ex.getMessage());
            } catch (Exception e) {
                Window.alert(e.getMessage());
            }
        }
    }


    /**
     * Writes the in-memory list of contacts to the local database.
     */
    private void putToLocalContacts() {
        ResultSet rs = null;

        for (ContactProxy cm : contacts.values()) {
            try {
                // try to find in local db
                int i = 0;
                rs = db.execute("select * from contact where id = ?", new String[]{cm.getId().toString()});
                for (i = 0; rs.isValidRow(); ++i, rs.next()) {
                    if (i > 1) {
                        Window.alert("More than one contact found in local db");
                        throw new Exception("More than one contact found in local db");
                    }
                    if (cm.getVersionNr() > rs.getFieldAsInt(4)) {
                        db.execute("update contact set firstName = ?, lastName = ?, " +
                                "addressID = ?, versionNr = ? where contact.id = ?", new String[]{cm.getFirstName(),
                                    cm.getLastName(), String.valueOf(cm.getAddressID()), cm.getVersionNr().toString(),
                                    cm.getId().toString()});
                    }
                }

                if (i == 0) {
                    // insert into database
                    db.execute("insert into contact (id, firstName, lastName, addressID, versionNr) values (?, ?, ?, ?, ?)",
                            new String[]{cm.getId().toString(), cm.getFirstName(), cm.getLastName(),
                                String.valueOf(cm.getAddressID()), cm.getVersionNr().toString()});
                }

                rs.close();
            } catch (DatabaseException ex) {
                Window.alert("Error while synchronizing contacts to local database\n" + ex.getMessage());
            } catch (Exception e) {
                Window.alert(e.getMessage());
            }
        }
    }


    //callbacks
    /**
     * This callback object gets the country list from the remote server,
     * then (synchronously) gets the address list, then (synchronously) gets the contact list.
     * This must be done synchronously, since country data must be available for the
     * address list to be created and addresses must be present before the contact list can be created.
     */
    final AsyncCallback<Vector<CountryProxy>> getRemoteCountries = new AsyncCallback() {

        public void onSuccess(Object result) {
            Vector<CountryProxy> countryList = (Vector<CountryProxy>) result;

            if (countryList == null || countryList.isEmpty()) {
//                Window.alert("got no countries from server");
            } else {
                for (CountryProxy c : countryList) {
                    countries.put(c.getId(), c);
                }
                getAddressService().getAddresses(getRemoteAddresses);
            }
        }


        public void onFailure(Throwable caught) {
//            Window.alert("Failed to get countries from server!\n" + caught.getMessage());
            // do nothing, list is filled with local data, if available
        }
    };


    /**
     * Callback object for getting the list of addresses from the remote server.
     */
    final AsyncCallback<Vector<AddressProxy>> getRemoteAddresses = new AsyncCallback() {

        public void onSuccess(Object result) {
            Vector<AddressProxy> addressList = (Vector<AddressProxy>) result;

            if (addressList == null || addressList.isEmpty()) {
                // TODO send local addresses to server
//                Window.alert("got no addresses from server");
            } else {
                for (AddressProxy a : addressList) {
                    if (addresses.containsKey(a.getId())) {
                        a = (AddressProxy)getNewerVersion(a, addresses.get(a.getId()));
                    }

                    addresses.put(a.getId(), a);
                }
                // write to local database
                putToLocalAddresses();

                getContactService().getContacts(getRemoteContacts);
            }
        }


        public void onFailure(Throwable caught) {
//            Window.alert("Failed to get addresses from server!\n" + caught.getMessage());
            // do nothing, list is filled with local data, if available
        }
    };


    /**
     * Callback object for getting the list of contacts from the remote server.
     */
    final AsyncCallback<Vector<ContactProxy>> getRemoteContacts = new AsyncCallback() {

        public void onSuccess(Object result) {
            // update contact list if necessary
            Vector<ContactProxy> contactList = (Vector<ContactProxy>) result;
            if (contactList == null || contactList.isEmpty()) {
//                Window.alert("got no contacts from server");
            } else {
                for (ContactProxy a : contactList) {
                    if (contacts.containsKey(a.getId())) {
                        a = (ContactProxy)getNewerVersion(a, contacts.get(a.getId()));
                    }
                    contacts.put(a.getId(), a);
                }
            }
            putToLocalContacts();

            // notify listeners about update
            fireUpdateEvent();
        }

        public void onFailure(Throwable caught) {
//            Window.alert("could not get contacts from server\n" + caught.getMessage());
            // notify listeners about update
            fireUpdateEvent();
        }
    };


    /**
     * Callback object for synchronization.
     * This objects creates a list of conflicts, if necessary.
     */
    final AsyncCallback<Vector<ChangeObject>> remoteUpdateCallback = new AsyncCallback() {

        public void onFailure(Throwable caught) {
            fireUpdateEvent();
        }

        public void onSuccess(Object result) {
            syncConflicts = (Vector<ChangeObject>) result;
            try {
                // clear the changes table
                db.execute("DELETE FROM changes");
            } catch (DatabaseException ex) {
                Window.alert(ex.getMessage());
            }
            fireUpdateEvent();
        }
    };


    /**
     * Updates the provided address in the local database.
     *
     * @param a
     * The address to be updated.
     */
    public void updateAddress(AddressProxy a) {
        try {
            db.execute("update address set city = ?, street = ?, " +
                    "zip = ?, countryID = ?, versionNr = ? where id = ?",
                    new String[]{a.getCity(), a.getStreet() == null ? "NULL" : a.getStreet(), a.getZip(),
                        a.getCountryID().toString(), a.getVersionNr().toString(), a.getId().toString()});

            // add to changes table, if not already present
            ResultSet rs = db.execute("select * from changes where objectID = ?", new String[]{a.getId().toString()});
            int i = 0;
            for (i = 0; rs.isValidRow(); ++i, rs.next()) ;
            if (i == 0)
                db.execute("insert into changes (objectID, typeName, changeType) values (?, ?, ?)",
                    new String[]{a.getId().toString(), ChangeObject.TYPE_ADDRESS, ChangeObject.MODIFIED});
        } catch (DatabaseException ex) {
            Window.alert("Cannot update address\n" + ex.getMessage());
        }
    }


    /**
     * Updates the provided contact in the local database.
     *
     * @param c
     * The contact to be updated.
     */
    public void updateContact(ContactProxy c) {
        try {
            db.execute("update contact set firstName = ?, lastName = ?, " +
                    "addressID = ?, versionNr = ? where contact.id = ?", new String[]{c.getFirstName(), c.getLastName(),
                    String.valueOf(c.getAddressID()), c.getVersionNr().toString(), c.getId().toString()});

            // add to changes table, if not already present
            ResultSet rs = db.execute("select * from changes where objectID = ?", new String[]{c.getId().toString()});
            int i = 0;
            for (i = 0; rs.isValidRow(); ++i, rs.next());
            if (i == 0) {
                db.execute("insert into changes (objectID, typeName, changeType) values (?, ?, ?)",
                        new String[]{c.getId().toString(), ChangeObject.TYPE_CONTACT, ChangeObject.MODIFIED});
            }
        } catch (DatabaseException ex) {
            Window.alert("Cannot update contact\n" + ex.getMessage());
        }
    }
    

    /**
     * Inserts a new address into the local database and into the in-memory list.
     *
     * @param a
     * The new address.
     */
    public void putNewAddress(AddressProxy a) {
        try {
            // insert into database (version nr. has default value of 1)
            db.execute("insert into address (id, city, street, zip, countryID) " + "values (?, ?, ?, ?, ?)",
                    new String[]{a.getId().toString(), a.getCity(), a.getStreet() == null ? "NULL" : a.getStreet(),
                        a.getZip(), a.getCountryID().toString()});

            // insert into changes table
            db.execute("insert into changes (objectID, typeName, changeType) values (?, ?, ?)",
                    new String[]{a.getId().toString(), ChangeObject.TYPE_ADDRESS, ChangeObject.NEW});
            addresses.put(a.getId(), a);
        } catch (Exception e) {
            Window.alert("Could not insert new address into local database\n" + e.getMessage());
        }

    }


    /**
     * Inserts a new contact into the local database and into the in-memory list.
     *
     * @param c
     * The new contact.
     */
    public void putNewContact(ContactProxy c) {
        try {
            db.execute("insert into contact (id, firstName, lastName, addressID, versionNr) values (?, ?, ?, ?, ?)",
                new String[]{c.getId().toString(), c.getFirstName(), c.getLastName(),
                    String.valueOf(c.getAddressID()), c.getVersionNr().toString()});
            // remember change in changes table
            db.execute("insert into changes (objectID, typeName, changeType) values (?, ?, ?)",
                    new String[]{c.getId().toString(), ChangeObject.TYPE_CONTACT, ChangeObject.NEW});
            contacts.put(c.getId(), c);
        } catch (Exception e) {
            Window.alert("Could not insert new contact into local database\n" + e.getMessage());
        }

    }


    /**
     * Deletes an address from the local database and from list.
     *
     * @param id
     * The id of the address to be deleted.
     */
    public void deleteAddress(final Integer id) {
        addresses.remove(id);
        try {
            // remove from local db
            db.execute("DELETE FROM address WHERE id = ?", new String[]{id.toString()});

            // remember change in changes table
            ResultSet rs = db.execute("SELECT * FROM changes WHERE objectID = ? AND typeName = ?",
                    new String[]{id.toString(), ChangeObject.TYPE_ADDRESS});
            int i = 0;
            for (i = 0; rs.isValidRow(); ++i, rs.next());
            if (i == 0)
                db.execute("INSERT INTO changes (objectID, typeName, changeType) VALUES (?, ?, ?)",
                    new String[]{id.toString(), ChangeObject.TYPE_ADDRESS, ChangeObject.DELETED});
            else db.execute("DELETE FROM changes WHERE objectID = ? AND typeName = ?",
                    new String[]{id.toString(), ChangeObject.TYPE_ADDRESS});
        } catch (DatabaseException ex) {
            Window.alert("could not remove address from local database\n" + ex.getMessage());
        }

    }


    /**
     * Deletes an contact from the local database and from list.
     *
     * @param id
     * The id of the contact to be deleted.
     */
    public void deleteContact(final Integer id) {
        try {
            // remove from local db
            db.execute("DELETE FROM contact WHERE id = ?", new String[]{id.toString()});

            // remember change in changes table
            ResultSet rs = db.execute("SELECT * FROM changes WHERE objectID = ? AND typeName = ?",
                    new String[]{id.toString(), ChangeObject.TYPE_CONTACT});
            int i = 0;
            for (i = 0; rs.isValidRow(); ++i, rs.next());
            if (i == 0)
                db.execute("INSERT INTO changes (objectID, typeName, changeType) VALUES (?, ?, ?)",
                    new String[]{id.toString(), ChangeObject.TYPE_CONTACT, ChangeObject.DELETED});
            else db.execute("DELETE FROM changes WHERE objectID = ? AND typeName = ?",
                    new String[]{id.toString(), ChangeObject.TYPE_CONTACT});
            deleteAddress(contacts.get(id).getAddressID());
            contacts.remove(id);
            fireUpdateEvent();
        } catch (DatabaseException ex) {
            Window.alert("could not remove contact locally\n" + ex.getMessage());
        }
        
    }


    // services
    public static ContactServiceAsync getContactService() {
        ContactServiceAsync service = (ContactServiceAsync) GWT.create(ContactService.class);
        ServiceDefTarget endpoint = (ServiceDefTarget) service;
        String moduleRelativeURL = GWT.getModuleBaseURL() + "contactservice";
        endpoint.setServiceEntryPoint(moduleRelativeURL);
        return service;
    }


    public static AddressServiceAsync getAddressService() {
        AddressServiceAsync service = (AddressServiceAsync) GWT.create(AddressService.class);
        ServiceDefTarget endpoint = (ServiceDefTarget) service;
        String moduleRelativeURL = GWT.getModuleBaseURL() + "addressservice";
        endpoint.setServiceEntryPoint(moduleRelativeURL);
        return service;
    }


    public static SyncServiceAsync getSyncService() {
        SyncServiceAsync service = (SyncServiceAsync) GWT.create(SyncService.class);
        ServiceDefTarget endpoint = (ServiceDefTarget) service;
        String moduleRelativeURL = GWT.getModuleBaseURL() + "syncservice";
        endpoint.setServiceEntryPoint(moduleRelativeURL);
        return service;
    }
    // end services //


    // event firing //
    /**
     * Adds a listener for UpdateEvents.
     * @param listener
     */
    public static void addUpdateListener(UpdateEventListener listener) {
        updateListeners.add(listener);
    }


    /**
     * Fires an UpdateEvent.
     */
    private void fireUpdateEvent() {
        for (UpdateEventListener listener : updateListeners) {
            listener.onUpdateDone();
        }
    }
    // end event firing //

    
    // getters and setters //
    public HashMap<Integer, ContactProxy> getContacts() {
        return contacts;
    }


    public HashMap<Integer, AddressProxy> getAddresses() {
        return addresses;
    }


    public HashMap<Integer, CountryProxy> getCountries() {
        return countries;
    }


    /**
     * Gets the country associated with the provided nickname.
     * 
     * @param nickName
     * The nickname of the country to get.
     *
     * @return
     * The country, if found, false otherwise.
     */
    public CountryProxy getCountryByNickName(String nickName) {
        for (CountryProxy c : countries.values()) {
            if (c.getNickName().equals(nickName)) return c;
        }
        return null;
    }


    /**
     * Returns a list of all country-nicknames.
     *
     * @return
     * A list of all country-nicknames.
     */
    public Vector<String> getCountryNickNames() {
        Vector<String> nickNames = new Vector<String>();
        for (CountryProxy c : countries.values()) nickNames.add(c.getNickName());
        return nickNames;
    }


    /**
     * Gets the address with the provided id.
     *
     * @param id
     * The id of the address.
     *
     * @return
     * The address with the provided id, if found, or null otherwise.
     */
    public AddressProxy getAddress(Integer id) {
        return addresses.get(id);
    }


    /**
     * Gets the contact with the provided id.
     *
     * @param id
     * The id of the contact.
     *
     * @return
     * The contact with the provided id, if found, or null otherwise.
     */
    public ContactProxy getContact(Integer id) {
        return contacts.get(id);
    }


    /**
     * Gets the next available id for an address.
     * 
     * @return
     * The next available id for an address.
     */
    public Integer getNextAddressId() {
        if (nextAddressID == null) {
            nextAddressID = 0;

            for (Integer id : addresses.keySet()) {
                if (id > nextAddressID) {
                    nextAddressID = id;
                }
            }
        }

        nextAddressID += 1;
        return nextAddressID;
    }


    /**
     * Gets the next available id for a contact.
     *
     * @return
     * The next available id for a contact.
     */
    public Integer getNextContactId() {
        if (nextContactID == null) {
            nextContactID = 0;
            for (Integer id : contacts.keySet()) {
                if (id > nextContactID) nextContactID = id;
            }
        }
        nextContactID += 1;
        return nextContactID;
    }


    /**
     * Gets the list of synchronization conflicts.
     *
     * @return
     * The list of synchronization conflicts.
     */
    public Vector<ChangeObject> getSyncConflicts() {
        return syncConflicts;
    }
    
}
